---
title: Custom components with fallbacks
description: Augmenting the default components
hide_table_of_contents: true
---

import { SandpackRQB } from '@site/src/components/SandpackRQB';

You may run into a situation where one of the default components _almost_ meets your requirements, but you don't want to recreate the entire component just to slightly modify the behavior. Falling back to the default component after implementing your custom behavior is a good way to keep your implementation up to date with the current version's standard features while retaining the flexibility of a fully custom solution.

For example, let's say you need a value editor that presents the user with a date picker (not the browser's default date picker), but only for certain fields. The default `ValueEditor` does not implement a date picker, so you'll need to use a custom component.

However, you don't need to copy/paste the default `ValueEditor` code to take advantage of its functionality. Simply spread the same props that were passed in to your custom component (`<ValueEditor {...props} />`) and return that if your custom behavior is not applicable.

Let's create a custom value editor that uses the [`react-datepicker`](https://reactdatepicker.com/) library. First we'll set up the `fields` array. Each element is a standard `Field` object, but the two date-type fields have a special attribute called `datatype` that will let our custom value editor know when and how to display the date picker.

```ts
// fields.ts
import { Field } from 'react-querybuilder';

export const fields: Field[] = [
  {
    name: 'name',
    label: 'Name',
    operators: [
      { name: '=', label: 'is' },
      { name: 'beginsWith', label: 'begins with' },
    ],
  },
  {
    name: 'dateOfBirth',
    label: 'Date of Birth',
    operators: [{ name: '=', label: 'is' }],
    datatype: 'date',
  },
  {
    name: 'dateRange',
    label: 'Date Range',
    operators: [{ name: 'between', label: 'is between' }],
    datatype: 'dateRange',
  },
];
```

Next, we'll define the custom value editor. The component will display a standard date picker if the `datatype` for the field is `"date"`, and a date _range_ picker if the `datatype` is `"dateRange"`. If the `datatype` is something else or `undefined`, then the component will simply forward its props to the default `ValueEditor`.

We're also using the [`date-fns`](https://date-fns.org/) library to help parse and format dates. Storing the date values as strings instead of `Date` objects helps ensure that the `query` object remains serializable in case we want to safely use `JSON.stringify`. (Date ranges are stored as a comma-separated pair of strings.)

```tsx
// CustomValueEditor.tsx
import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { ValueEditor, ValueEditorProps } from 'react-querybuilder';

const dateFormat = 'yyyy-MM-dd';

export const CustomValueEditor = (props: ValueEditorProps) => {
  if (props.fieldData.datatype === 'date') {
    return (
      <div>
        <DatePicker
          dateFormat={dateFormat}
          selected={!props.value ? null : parse(props.value, dateFormat, new Date())}
          onChange={(d: Date) => props.handleOnChange(d ? format(d, dateFormat) : null)}
        />
      </div>
    );
  } else if (props.fieldData.datatype === 'dateRange') {
    const [startDate, endDate] = props.value.split(',');
    return (
      <div>
        <DatePicker
          selectsRange
          dateFormat={dateFormat}
          startDate={!startDate ? null : parse(startDate, dateFormat, new Date())}
          endDate={!endDate ? null : parse(endDate, dateFormat, new Date())}
          onChange={(update: [Date, Date]) => {
            const [s, e] = update;
            props.handleOnChange(
              [!s ? '' : format(s, dateFormat), !e ? '' : format(e, dateFormat)].join(',')
            );
          }}
        />
      </div>
    );
  }
  return <ValueEditor {...props} />;
};
```

:::tip

If you're using one of the [compatibility packages](../compat), you probably want to fall back to the value editor from that package instead of `ValueEditor` from the main package. For example, when using `@react-querybuilder/antd`, fall back to `AntDValueEditor`:

```diff
-import { ValueEditor, ValueEditorProps } from 'react-querybuilder';
+import { AntDValueEditor } from '@react-querybuilder/antd';
+import { ValueEditorProps } from 'react-querybuilder';
```

```diff
-  return <ValueEditor {...props} />;
+  return <AntDValueEditor {...props} />;
```

:::

Finally, we can configure the main `QueryBuilder` component to use our custom value editor with the `controlElements` prop.

```tsx
// App.tsx
import { useState } from 'react';
import { CustomValueEditor } from './CustomValueEditor';
import { fields } from './fields';

export default function App() {
  const [query, setQuery] = useState({ combinator: 'and', rules: [] });
  return (
    <QueryBuilder
      fields={fields}
      query={query}
      onQueryChange={q => setQuery(q)}
      // highlight-start
      controlElements={{ valueEditor: CustomValueEditor }}
      // highlight-end
    />
  );
}
```

An interactive demo is below. Note how the "Name" field uses a text input, the "Date of Birth" field uses a standard date picker, and the "Date Range" field uses the date range picker.

<SandpackRQB rqbVersion={4} customSetup={{ dependencies: { 'date-fns': 'latest', 'react-datepicker': 'latest' } }} options={{ editorHeight: 690 }}>

```tsx
import { useState } from 'react';
import { QueryBuilder } from 'react-querybuilder';
import { CustomValueEditor } from './CustomValueEditor';
import { fields } from './fields';
import { initialQuery } from './initialQuery';

export default function App() {
  const [query, setQuery] = useState(initialQuery);
  return (
    <QueryBuilder
      fields={fields}
      query={query}
      onQueryChange={q => setQuery(q)}
      controlElements={{ valueEditor: CustomValueEditor }}
    />
  );
}
```

```tsx CustomValueEditor.tsx active
import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { ValueEditor, ValueEditorProps } from 'react-querybuilder';

const dateFormat = 'yyyy-MM-dd';

export const CustomValueEditor = (props: ValueEditorProps) => {
  if (props.fieldData.datatype === 'date') {
    return (
      <div>
        <DatePicker
          dateFormat={dateFormat}
          selected={!props.value ? null : parse(props.value, dateFormat, new Date())}
          onChange={(d: Date) => props.handleOnChange(d ? format(d, dateFormat) : null)}
        />
      </div>
    );
  } else if (props.fieldData.datatype === 'dateRange') {
    const [startDate, endDate] = props.value.split(',');
    return (
      <div>
        <DatePicker
          selectsRange
          dateFormat={dateFormat}
          startDate={!startDate ? null : parse(startDate, dateFormat, new Date())}
          endDate={!endDate ? null : parse(endDate, dateFormat, new Date())}
          onChange={(range: [Date, Date]) => {
            const [s, e] = range;
            props.handleOnChange(
              [!s ? '' : format(s, dateFormat), !e ? '' : format(e, dateFormat)].join(',')
            );
          }}
        />
      </div>
    );
  }
  return <ValueEditor {...props} />;
};
```

```ts fields.ts
import { Field } from 'react-querybuilder';

export const fields: Field[] = [
  {
    name: 'name',
    label: 'Name',
    operators: [
      { name: '=', label: 'is' },
      { name: 'beginsWith', label: 'begins with' },
    ],
  },
  {
    name: 'dateOfBirth',
    label: 'Date of Birth',
    operators: [{ name: '=', label: 'is' }],
    datatype: 'date',
  },
  {
    name: 'dateRange',
    label: 'Date Range',
    operators: [{ name: 'between', label: 'is between' }],
    datatype: 'dateRange',
  },
];
```

```ts initialQuery.ts
import { format, subDays } from 'date-fns';
import { RuleGroupType } from 'react-querybuilder';

const initialRange = [subDays(new Date(), 14), new Date()]
  .map(d => format(d, 'yyyy-MM-dd'))
  .join(',');

export const initialQuery: RuleGroupType = {
  rules: [
    {
      field: 'name',
      operator: '=',
      value: 'Steve Vai',
    },
    {
      field: 'dateOfBirth',
      operator: '=',
      value: '1960-06-06',
    },
    {
      field: 'dateRange',
      value: initialRange,
      operator: 'between',
    },
  ],
  combinator: 'and',
  not: false,
};
```

```css
.react-datepicker-wrapper input {
  /* Widen the input to show both dates */
  width: 180px;
}
```

</SandpackRQB>

:::note

Other examples of the "fallback" technique can be seen in the [Limit rule groups](./limit-groups#conditionally-allow-new-groups) page and [these](https://stackoverflow.com/questions/68447510/react-query-builder-question-is-there-a-way-to-disable-a-field-option-when-addi/69443288#69443288) [two](https://stackoverflow.com/questions/61768845/progamatically-show-hide-operator-rule-and-group-button-in-react-querybuilder/69443467#69443467) StackOverflow answers.

:::
